An indepth look at 10 years of data from my Fantasy Football League.
Author
Jeff Milliman
Published
February 20, 2026
Introduction
Having played consistently in a fantasy football league for about ten years, often with poor and inconsistent result, I have often wondered about the extent to which draft order in fantasy football really matters. While draft order is often randomized in most leagues, the conventional folk wisdom seems to suggest that it is better to have an early pick than to have a later draft pick.
At first glance this makes sense. Many of us can recall fantasy football seasons where one or two superstar players had such an out sized effect on points scored that they single-handedly propelled their teams to victory. Obvious examples include LaDainian Tomlinson in the early 2000s, Peyton Manning’s seasons with the Denver Broncos in the 2010s, and Jonathon Taylor earlier this season. However, predicting which player will be a superstar in any given year is hard, and injuries to a couple of key players can often derail promising fantasy seasons.
In addition, fantasy draft order formats, like the snake draft, can mitigate draft order effects by reversing the order of draft picks every round. This suggests that draft pick order might be overrated. The ability to pick three or four good rather than one or two fantastic players might pay dividends overtime and reduce the risk of inconsistent play or injuries.
Thanks to the fantastic ffscrapr R package and the Espn API I have been able to download my fantasy football league’s data directly into R, giving me an opportunity to analyze the impact of draft order on league outcomes. In addition, I rank league draft positions by their impact on fantasy football outcomes and simulate 1000 fantasy football seasons to get a sense of whether particular players have had especially lucky or unlucky draft positions relative to what may be expected from the simulated drafts positions. All fantasy manager team names and personal identifying information have been removed for the analysis.
Loading in the Data
Code
#load in packageslibrary(tidyverse) #general data manipulationlibrary(readr) #loading in csvlibrary(gt) #tableslibrary(fixest) #fix effects regressionslibrary(infer) #repeat sampling simulationslibrary(marginaleffects) #marginal effectslibrary(modelsummary) #summarizing regression tableslibrary(Hmisc) #potting error bars#load in the clean csv with season outcomesff_data <-read_csv("D5_Draft_position_season_outcomes_clean.csv")#make previous season rank by franchise member name - lag league rankff_data <- ff_data |>arrange(year) |>group_by(unique_id) |>mutate(prior_league_rank =lag(league_rank, order_by = unique_id,default =0)) |>ungroup()
Table 1: League format and scoring by year
Code
# create table of descriptive statistics by yeartable_1 <- ff_data |>group_by(year) |> dplyr::mutate(avg_points_scored =mean(points_for),scoring_flags =case_when(scoring_flags =="zero_ppr"~"Zero PPR", scoring_flags =="0.5_ppr"~"0.5 PPR"),min_points_scored =min(points_for),max_points_scored =max(points_for)) |> dplyr::select(year, scoring_flags, franchise_count, roster_size, franchise_count, avg_points_scored, min_points_scored, max_points_scored) |>#Move from individual to group level stats dplyr::distinct(year, .keep_all =TRUE) |> dplyr::rename("Year"= year,"Avg. Points Scored"= avg_points_scored,"Scoring Format"= scoring_flags,"Franchise Count"= franchise_count,"Roster Size"= roster_size,"Minimum Points Scored"= min_points_scored,"Maximum Points Scored"= max_points_scored) |>ungroup() |>#Make into a gt table objectgt() |>#Add in the titletab_header(title ="League Format and Scoring Stats by Year") |> gt::tab_options(row.striping.include_table_body =FALSE,quarto.disable_processing =TRUE)table_1
League Format and Scoring Stats by Year
Year
Scoring Format
Franchise Count
Roster Size
Avg. Points Scored
Minimum Points Scored
Maximum Points Scored
2016
Zero PPR
10
16
1149.700
983.00
1318.00
2017
Zero PPR
10
16
1095.100
947.00
1238.00
2018
Zero PPR
12
16
1349.162
1110.02
1544.64
2019
Zero PPR
12
16
1276.208
1184.34
1423.98
2020
Zero PPR
12
16
1301.627
1083.64
1467.16
2021
0.5 PPR
12
17
1476.678
1387.46
1688.26
2022
0.5 PPR
12
17
1420.497
1048.36
1592.86
2023
0.5 PPR
12
17
1450.357
1351.42
1584.74
2024
0.5 PPR
12
17
1433.735
1087.14
1606.96
In Table 1 above, I display the years, league size, scoring format, and average, minimum, and maximum points scored (points for) by the year of my fantasy football league. The league has nine years of full data, starting with the first season in 2016 and through the last completed season, 2024. For the first five seasons league scoring was zero points per reception, before switching to 0.5 points per reception in 2021. The average points scored increased approximately 150 to 200 points per year following the change from a zero ppr to a 0.5 ppr scoring format. For the first two seasons the number of fantasy teams was 10, before switching to a 12 team league in 2018. For all league years the draft format was a randomized, snake draft.
Statistical Analysis Strategy
With the data loaded in and the descriptive statistics covered, I’ll move onto a brief overview of the methods I will use to analyze draft order effects. The really nice thing about draft order in our league is that it is truly random every year. This means that there is no selection into treatment: draft order does not depend on previous results or individual skill level. However, as individual ability/knowledge might have an independent effect on fantasy football success we can get a sense of the impact of draft order by comparing an individual’s performance year to year with a different draft pick order. This is essentially a within-subject experiment where you serve as your own control group.
This is an attractive design, as it doesn’t require a large control group, but suffers from three obvious problems:
1) Low Statistical Power: With 10 to 12 players and therefore 10 to 12 treatment conditions (draft pick order), there is only a small number of observations per year and per person overtime. This means that the effects of draft order likely need to be quite large to detect a statistically significant effect, especially with a sample size that is around 90 to 120 observations over 9 years of the league.
2)Assumes fantasy football ability is relatively fixed and that there are no draft order effects across years: One of the most compelling criticisms of within-subject experiments is that the time period in which you are randomly assigned to the treatment may influence how you respond to the treatment. For example, if I am exposed to treatment condition A first at T1 and then treatment condition B at T2, even if the order of exposure is randomized, I have to believe that this order does not impact how I respond to the treatment. This is a more believable assumption if the experiment does not last very long, say a day or so, but becomes much less believable overtime as it becomes much harder to say that you are truly the same person at time period 1 (T1) as you are at time period 2 (T2).
As the league has gone on for 10 years, this assumption requires us to assume that individual fantasy football ability has stayed relatively fixed over the length of the league - i.e. my ability/attention to fantasy football in 2017 is similar to my ability/attention in 2025. The extent to which this assumption holds is debatable. Life events, grad school, and stress have certainly made me less attentive to fantasy football overtime, which suggests that I am not the same fantasy player as I was in 2017.
A particularly bad or good season may also influence my performance in a subsequent season. In addition, as the early seasons included some mild “punishments” for last place that were not always enforced, the incentive to not be last may have had a larger impact on fantasy performance in earlier seasons.
That being said, I do think there might be some consistency overtime in individual knowledge and approach to fantasy football that makes this assumption somewhat tenable. This assumption can also be relaxed by adding in some control variables that might help to account for changes in individual effort/skill overtime.
3)Unequal exposure to treatment conditions: In a traditional within-subject experiment every subject is exposed to every treatment condition, but the order of treatment exposure is randomized. If that were true for this league everyone would be exposed to every draft pick position, but the order in which they would receive each draft position would be randomized. Unfortunately, that is not the case here. While randomization of draft order occurs every year, draft positions can be repeated year to year, which means that not everyone has been exposed to the full range of fantasy draft positions.
As a result, the model has to extrapolate over the range of draft pick orders by assuming that the impact of draft pick order is relatively constant over the range of draft picks. For example, if I have never had a draft order lower than 10th in a 12 person league, the model will assume that my own performance over picks 1 to 10 provides a good counter-factual for how I would have performed if I had drafted at positions 11 and 12. Accounting for individual effects, the model also has to assume that the performance of other players over draft positions 11 and 12 provides a good counter-factual to how I would have performed had I received those draft positions.
Variables
With the caveats to the research design out of the way, lets move onto to the outcomes, treatment, and control variables.
Outcome Variables:
To analyze the impact of draft order on performance, my analysis includes three outcome variables:
1)Points For: The average points scored per draft position over each season.
2)Win Percentage: The average win percentage by draft position over each season.
3)Season Wins: The number of wins at the end of each season per draft position. This is a essentially a slightly more intuitive version of Win Percentage.
Independent (Treatment) Variable:
1)Draft Position: The draft pick position randomized at the start of each season. Note that the draft position is not revealed until right before the draft which means that players have minimal opportunity to change their draft strategy at the last minute in response to their draft position.
Control Variables
To increase statistical power and account for possible time varying factors related to the outcomes I’ll throw in the following controls:
1) League Scoring Format: A Binary dummy variable indicating points 0.5 ppr or Zero ppr. As the league scoring format may have a large impact on points for and win percentage, it makes sense to account for the scoring format in the analysis.
2) Years Prior Experience: More experience in the league may give some players an advantage in terms of knowledge and draft strategy. As there has been some turnover in the league, some players have more experience than others which may lead to improved performance overtime.
3) League Standings in the Previous Year: An especially strong or weak showing in a previous year may motivate players to try harder in the next season or lull them into a sense of complacency. For now, the analysis will include league standings in the previous year as a categorical variable with years in which there were no previous league standings - i.e. a player’s first year in the league - set to the reference category of 0 to avoid dropping years with no previous year league standings.
Preliminary Plots
Before moving onto the statistical models, I’ll display a series of plots analyzing the bivariate relationship between draft order, points scored, and win percentage to get a sense of underlying trends. In the first series of plots I will treat draft order as a continuous variable to get a sense of underlying relationship between draft order, points scored and win percentage over the full range of draft order picks. To account for possible nonlinear relationships between draft order, points scored, and win percentage, I fit a LOESS (locally-estimated scatter-plot smoothing) regression line over the range of draft order picks.
In the second series of plots I will treat draft order as a categorical variable, plotting points scored and win percentage by draft position, along with the average points scored and win percentage and 95% confidence intervals for the averages.
Plots: Treating draft order as continuous
Code
plot_1 <- ff_data |>mutate(scoring_flags =as.factor(scoring_flags),scoring_flags =case_when(scoring_flags =="zero_ppr"~"Zero PPR", scoring_flags =="0.5_ppr"~"0.5 PPR")) |>ggplot(aes(x = draft_pick_order, y = points_for)) +geom_point(position =position_jitter(.1), size =1.2) +geom_smooth(method ="loess") +labs(title ="Figure 1: Relationship between Draft Order and Points For",x ="Draft Pick Order",y ="Points For",caption ="The blue line represents the LOESS regression fit line at each draft pick.\n The grey shaded region represent 95% confidence intervals around the blue LOESS regression line.") +scale_x_continuous(breaks =seq(from =0, to =12, by =1)) +scale_y_continuous(breaks =seq(from =900, to =1800, by =100), limits =c(900,1800))+theme_minimal() +facet_grid(~ scoring_flags) +theme_bw()plot_1
Code
##impact of Draft order on Win percentageplot_2 <- ff_data |>mutate(h2h_winpct = (h2h_winpct *100),scoring_flags =as.factor(scoring_flags),scoring_flags =case_when(scoring_flags =="zero_ppr"~"Zero PPR", scoring_flags =="0.5_ppr"~"0.5 PPR")) |>ggplot(aes(x = draft_pick_order, y = h2h_winpct, group = scoring_flags)) +geom_point(position =position_jitter(.1), size =1.2) +geom_smooth(method ="loess") +labs(title ="Figure 2: Relationship between Draft Order and Win Percentage",x ="Draft Pick Order",y ="Win Percentage",caption ="The blue line represents the LOESS regression fit line at each draft pick.\n The grey shaded region represent 95% confidence intervals around the blue LOESS regression line.") +scale_x_continuous(breaks =seq(from =0, to =12, by =1)) +scale_y_continuous(breaks =seq(from =0, to =100, by =10)) +facet_grid(~ scoring_flags) +theme_bw()plot_2
Figures 1 and 2 above plot the relationship between draft order, points for (Figure 1), and win percentage (Figure 2), by scoring format (0.5 ppr and zero ppr). The blue line is the LOESS regression fit line at each draft pick position and the shaded region is the 95% confidence interval around the LOESS regression line.
Looking at Figure 1, two things stand out to me:
1)Scoring format has a strong influence on the relationship between draft order and points for. In the zero ppr format points for scored decreases noticeably around draft positions 6 and 7, before climbing at draft positions 11 and 12. While this dip is also present in the 0.5 ppr format, it is much less pronounced than in the zero ppr league.
2)The variance or spread of the points for (points scored) by draft position is much higher in the zero ppr scoring format than in the 0.5 ppr format. As we can see, points for are much more clustered around the 95% confidence interval of the regression line in the 0.5 ppr format than in the zero ppr format. Therefore, switching from zero ppr to 0.5 ppr appears to have minimized the impact of draft order on points for and decreased the variance of points for, making league scoring more competitive overall.
In Figure 2 the relationship between draft order and win percentage across scoring formats follows similar trends as they did for points for in Figure 1. The only noticeable difference is that is the variance of win percentage is much higher across both scoring formats, which suggests that points for does not always translate into a high win percentage, although the relationship between the two is likely strong and positive
Plots: Treating draft order as categorical
Code
#Plot 3 - Impact of draft order on points for##Plot 4 - impact of draft pick order on points for plot_3 <- ff_data |>mutate(draft_pick_order =as.factor(draft_pick_order),scoring_flags =as.factor(scoring_flags),scoring_flags =case_when(scoring_flags =="zero_ppr"~"Zero PPR", scoring_flags =="0.5_ppr"~"0.5 PPR")) |>ggplot(aes(x = draft_pick_order, y = points_for, group = scoring_flags)) +geom_point(position =position_jitter(0)) +stat_summary(geom ="point", fun.data = mean_cl_normal, color ="red", size =2, position =position_nudge(.3)) +stat_summary(geom ="errorbar", fun.data = mean_cl_normal,color ="red", width =0, position =position_nudge(.3)) +labs(title ="Figure 3: Relationship between Draft Order and Points For",x ="Draft Pick Order",y ="Points For",caption ="The red points represent the mean points for at each draft pick.\n The red error bars represent 95% confidence intervals around the mean points for.") +scale_x_discrete(breaks =seq(from =0, to =12, by =1)) +scale_y_continuous(breaks =seq(from =900, to =1800, by =100), limits =c(900,1800)) +facet_grid(~ scoring_flags) +theme_bw()plot_3
Code
plot_4 <- ff_data |>mutate(draft_pick_order =as.factor(draft_pick_order),scoring_flags =as.factor(scoring_flags),scoring_flags =case_when(scoring_flags =="zero_ppr"~"Zero PPR", scoring_flags =="0.5_ppr"~"0.5 PPR"),h2h_winpct = (h2h_winpct*100)) |>ggplot(aes(x = draft_pick_order, y = h2h_winpct, group = scoring_flags)) +geom_point(position =position_jitter(.1)) +stat_summary(geom ="point", fun.data = mean_cl_normal, color ="red", size =2, position =position_nudge(.3)) +stat_summary(geom ="errorbar", fun.data = mean_cl_normal,color ="red", width =0, position =position_nudge(.3)) +labs(title ="Figure 4: Relationship between Draft Order and Win Percentage",x ="Draft Pick Order",y ="Win Percentage",caption ="The red points represent the mean win percentage at each draft pick.\n The red error bars represent 95% confidence intervals around the mean win percentage. ") +scale_x_discrete(breaks =seq(from =0, to =12, by =1)) +scale_y_continuous(breaks =seq(from =0, to =100, by =10), limits =c(0,105)) +facet_grid(~ scoring_flags) + ggplot2::geom_hline(yintercept =50,linetype =2) + ggplot2::geom_hline(yintercept =100,linetype =1) +theme_bw()plot_4
In Figures 3 and 4, I plot the relationship between draft order, points for (Figure 3) and win percentage (Figure 4), treating draft order as a categorical rather than continuous variable. The advantage of treating draft order as a categorical variable is this allows for the plotting of the mean points for and win percentage by draft position, along with a 95% confidence interval around the mean, that might better display the range of the outcomes at each draft position without trying to fit an overall trend line through the range of draft picks.
Figures 3 and 4 show similar trends as Figures 1 and 2, but those trends become more pronounced when treating draft pick order as a categorical variable. In Figure 3, draft positions 3 and 7 stand out for their low average points scored scored and wide confidence intervals around the mean (red points and red error bars) in the 0.5 ppr format. For the zero ppr format draft positions 2, 6, and 8, are especially low, with draft positions 11 and 12 having the largest average points scored. This is also true for the 0.5 ppr format where draft positions 1, 10, 11, and 12 have a higher average and lower variance around the average.
In Figure 4, the relationship between draft order and win percentage is more pronounced. In the 0.5 ppr format, draft positions 2 and 7 stand out for how low the average win percentage is. For draft position 2 in the 0.5 ppr format, the average win percentage is around 40% with no season at that draft position over 50%. For draft position 7 in the 0.5 ppr format, three out of the four seasons have an abysmal win percentage under 40% and only one season has a win percentage over 50%. In the zero ppr format, draft positions 6, 7, 8, and 9 have a much lower average win percentages than draft positions at the beginning or end of the draft order.
Statistical Analysis
While the preliminary plots appear to suggest that points for and win percentage tend to be lower at draft positions at the middle of draft pick order and higher at the tails (draft positions 1 and 12) ,we can get some confirmation of this relationship by running some basic regression models, accounting for individual fixed effects and controls strongly related to the outcome.
In order to leverage repeat draft randomization on outcomes overtime within individuals and to reduce the impact of league size, I will focus my analysis only members of the league who have been in the league for at least four seasons and for league years in which the size of the league was 12 teams. This has the downside of reducing an already small sample size, but might do a better job of leveraging the advantages of repeat randomization overtime at the individual level.
The regression models, fit with the feols function from the fixest package, include years of prior experience as a covariate, and fixed effects for scoring format, previous league standings (place in the league in the previous year), and player id, with standard errors clustered by player id. In my analysis below, I forgo the usual display of regression tables in favor of coefficient plots of the Average Treatment Effects by draft position relative to a reference draft position.
While I have seen other within-subject experiments add in time (year) fixed effects or random effects for subjects, there does not seem to be a standard way to analyze these experiments. As I am more familiar with the fixed effects approach in political science and economics rather than the way these experiments are analyzed in psychology, I will stick with fixed effects at the subject (player id) level for now. For alternative approaches to analyzing these types of experiments, Solomon Kurz has a fantastic blog post on analyzing within-subject designs with multilevel models from both a frequentist and bayesian perspective.
Plots of Average Treatment Effects
Code
#subset data for franchise count == 12, years in league > 6og_ff <- ff_data |>filter(years_in_league >=4, franchise_count ==12) ##set reference level as pick 1 for points for modelog_ff$draft_pick_order <- fixest::ref(og_ff$draft_pick_order, ref =1)#Model 2 = points for with fixed effects for franchise member, scoring flags,#prior league rankmodel_2 <-feols(points_for ~ draft_pick_order + years_prior_ex|unique_id + scoring_flags + prior_league_rank, data = og_ff) #Marginal effects for model 2m1 <- marginaleffects::avg_comparisons(model_2, variables ="draft_pick_order")#set factor order for points for plotsfactor_levelsa <-c("2 - 1","3 - 1","4 - 1","5 - 1","6 - 1","7 - 1","8 - 1","9 - 1","10 - 1","11 - 1","12 - 1")#Plot ATE estimates for Points For modelplot_m1 <- m1 |>mutate(model_contrast =factor(contrast, levels = factor_levelsa ))|>ggplot(aes(y = estimate, x = model_contrast)) +geom_point(size =2) +#plot confidence intervalsgeom_errorbar(aes(ymin = conf.low, ymax = conf.high), width = .075) +scale_y_continuous(breaks =seq(from =-450, to =350, by =50),limits =c(-450, 350)) +geom_hline(yintercept =0, linetype =2) +labs(title ="Figure 4: ATE of Draft Order on Points For",y ="ATE (Average Difference in Points For)",x ="Contrasts",caption ="The error bars represent 95% confidence intervals. The points are ATE estimates.\n The reference level for comparison is the 1st draft position.") +theme_classic()plot_m1
Figure 4 above plots the Average Treatment Effect (ATE) of draft order on points scored by draft position. The ATE estimates display the average difference in points for at each draft position relative to draft position 1, which is closest to the average points scored across all years. As all of the confidence intervals include zero, none of the draft positions have a statistically significant effect on points for at p < .05.
However, relative to draft position 1, draft position 7 has on average approximately 170 less points for and draft position 2 has on average approximately 130 less points for. While these differences are large, the wide confidence intervals indicate substantial uncertainty around the estimates.
Code
##set reference level as pick 12 for points for modelog_ff$draft_pick_order <- fixest::ref(og_ff$draft_pick_order, ref =6)##win percentage as outcomemodel_3 <-feols(h2h_winpct ~ draft_pick_order + years_prior_ex|unique_id + scoring_flags + prior_league_rank, data = og_ff) #Marginal effects for model 3m2 <- marginaleffects::avg_comparisons(model_3, variables ="draft_pick_order")#set factor order for points for plotsfactor_levelsb <-c("1 - 6", "2 - 6","3 - 6","4 - 6","5 - 6","7 - 6","8 - 6","9 - 6","10 - 6","11 - 6","12 - 6")#Plot ATE estimates for Win Percentage Modelplot_m2 <- m2 |>mutate(model_contrast =factor(contrast, levels = factor_levelsb),estimate = (estimate *100),conf.high = (conf.high *100),conf.low = (conf.low *100))|>ggplot(aes(y = estimate, x = model_contrast)) +geom_point(size =2) +#plot confidence intervalsgeom_errorbar(aes(ymin = conf.low, ymax = conf.high), width = .075) +scale_y_continuous(breaks =seq(from =-50, to =50, by =10),limits =c(-50, 50)) +geom_hline(yintercept =0, linetype =2) +labs(title ="Figure 5: ATE of Draft Order on Win Percentage",y ="ATE (Average Difference in Win Percentage)",x ="Contrasts",caption ="The error bars represent 95% confidence intervals. The points are ATE estimates.\n The reference level for comparison is the 6th draft position.") +theme_classic()plot_m2
Figure 5 plots the ATE of draft order on win percentage, comparing each draft position to draft position 6, which is closest to the overall average win percentage and average wins across all draft positions. Draft positions 4 has a statistically significant higher ATE, although the confidence interval is just outside of 0. Relative to draft position 6, draft position 4 has a statistically significant average increase in win percentage of approximately 20%. Draft positions 7 and 9 have significantly lower average win percentages relative to draft position 6, but the results are not statistically significant at p < .05.
Code
#Model for h2h winsmodel_4 <-feols(h2h_wins ~as.factor(draft_pick_order) + years_prior_ex|unique_id + scoring_flags +prior_league_rank, data = og_ff) #Marginal effects for h2h winsm3 <- marginaleffects::avg_comparisons(model_4, variables ="draft_pick_order")#Plot ATE estimatesplot_m3 <- m3 |>mutate(model_contrast =factor(contrast, levels = factor_levelsb))|>ggplot(aes(y = estimate, x = model_contrast)) +geom_point(size =2) +#plot confidence intervalsgeom_errorbar(aes(ymin = conf.low, ymax = conf.high), width = .075) +scale_y_continuous(breaks =seq(from =-6, to =5, by = .5),limits =c(-6, 5)) +geom_hline(yintercept =0, linetype =2) +labs(title ="Figure 6: ATE of Draft Order on Season Wins",y ="ATE (Average Difference in season wins)",x ="Contrasts",caption ="The error bars represent 95% confidence intervals. The points are ATE estimates.\n The reference level for comparison is the 6th draft position.") +theme_classic()plot_m3
In order to make the differences in win percentage easier to understand, Figure 6 plots the impact of draft order on season wins. Relative to draft position 6, draft position 4 has a statistically significant increase of approximately 2.5 season wins on average. Draft position 7 has 2 less wins on average, while draft positions 9 and 2 have about 1 less wins on average. The ATE estimates for draft positions 7, 9, and 2 are not statistically significant at p < .05.
Has the draft order favored any league members?
The previous analysis shows that there is some variation in fantasy football outcomes by draft position. Draft positions in the middle and towards the end of the draft order, 7 and 9, and to some extent positions 8 and 2, have much lower win percentages and season wins, while draft position 4 has much higher season wins and win percentages on average.
As draft positions can be repeated, even though they are randomized every year, a natural extension of the previous analysis is to ascertain whether any players have received particularly good or bad draft positions based on the the impact of draft position on end of season win percentage.
To do this, I will use the predicted win percentages for each draft position from the regression model to rank the draft positions by their predicted win percentage from highest win percentage to lowest win percentage. For example, because draft position 7 has the lowest predicted win percentage, draft position 7 would receive the least favorable draft rank of 12th, and so on.
Then, I will join these rankings with the historical draft data to compute the average favorable draft position, median favorable draft position, and counts of how many times a player received a last place draft position, or a bottom four draft position (all positions with a predicted win percentage below 50%).
Ranking Fantasy Football Draft Positions by their impact on Win Percentage
Table 2 below ranks each draft position based on their predicted win percentage.
Code
#Extract model predictionspredicted_win_pct_draft_order_12 <- marginaleffects::avg_predictions(model = model_3,variables ="draft_pick_order",vcov =FALSE) |>#Make into a dataframeas.data.frame() |>#sort highest to lowest dplyr::arrange(desc(estimate)) |>#add in rowid for ranking tibble::rowid_to_column(var ="favorable_draft_position") |> dplyr::select(favorable_draft_position:estimate) |>mutate(estimate =round(estimate, digits =3))table_2 <- predicted_win_pct_draft_order_12 |> dplyr::rename("Favorable Draft Ranking"= favorable_draft_position,"Draft Position"= draft_pick_order,"Predicted Win Percentage"= estimate) |> gt::gt() |> gt::tab_header(title ="Table 2: Favorable Draft Ranking by Predicted Win Percentage") |> gt::tab_options(row.striping.include_table_body =FALSE,quarto.disable_processing =TRUE)table_2
Table 2: Favorable Draft Ranking by Predicted Win Percentage
Favorable Draft Ranking
Draft Position
Predicted Win Percentage
1
4
0.676
2
12
0.568
3
11
0.555
4
1
0.538
5
10
0.527
6
5
0.505
7
3
0.502
8
6
0.501
9
2
0.445
10
8
0.440
11
9
0.420
12
7
0.336
Code
#Join player data (in league for 4 years, 12 team format) #with the predicted in by draft orderLeague_out_12_teams <-left_join(og_ff, predicted_win_pct_draft_order_12, by ="draft_pick_order")#Create Table threetable_3 <- League_out_12_teams |>#Group by franchise member namegroup_by(unique_id) |>#Compute average, median, bottom, and bottom 4 dplyr::summarise(average_fav_win_pct =round(mean(favorable_draft_position),digits =3), median_fav_win_pct =median(favorable_draft_position),count_7 =sum(draft_pick_order ==7),count_bottom_4_win_pct =sum(favorable_draft_position >=9)) |>arrange(desc(median_fav_win_pct)) |>ungroup() |>#Rename columns for table dplyr::rename("Average favorable draft position"= average_fav_win_pct,"Median favorable draft position"= median_fav_win_pct,"Count draft position 7 (worst)"= count_7,"Count bottom four draft positions"= count_bottom_4_win_pct,"Player ID"= unique_id) |>gt() |> gt::tab_header(title ="Table 3: Player draft order ranked by favorable draft position") |> gt::tab_options(row.striping.include_table_body =FALSE,quarto.disable_processing =TRUE)table_3
Table 3: Player draft order ranked by favorable draft position
Player ID
Average favorable draft position
Median favorable draft position
Count draft position 7 (worst)
Count bottom four draft positions
player_7
7.571
9
1
4
player_9
7.286
9
0
4
player_15
7.429
8
1
3
player_8
7.571
8
0
3
player_6
7.000
7
0
2
player_13
5.800
6
0
1
player_2
5.667
6
0
1
player_4
6.714
6
1
3
player_18
5.000
5
0
0
player_5
6.000
5
0
1
player_19
5.500
4
1
1
player_17
4.571
2
0
2
Table 3 above ranks each player by how favorable their average and median draft positions has been by predicted win percentage, provided they have been in the league for at least four seasons and only for the 12 team league format (7 seasons). If draft positions were completely fair each player should have an average favorable and median favorable draft position around 6. Lower average and median favorable draft positions indicate more favorable draft positions overall, while higher average and median favorable draft positions indicate less favorable draft positions overall.
As we can see, Player 17 has clearly had the most favorable average and median draft positions, with an average draft position of 4.571 and an incredible median favorable draft position of 2, indicating that half of his draft positions were at or lower than the second best draft position. In addition, Player 17 has never drawn the worst draft position (7) and only has two bottom four draft positions in 7 years of league play. This is surprising because Player 17 has consistently performed at or near the bottom in league play. On the other hand, Player 7, Player 8, Player 9, and Player 15 have had consistently bad draft positions, with Player 7 having the highest (least favorable) median and average overall draft positions by win percentage.
Simulation of 1,000 Seven Season League Drafts
To get a sense of how likely the least and most favorable draft positions were by chance, I simulated the fantasy draft process 1,000 times, computing the average draft position and median draft position in a repeat randomized 12 person draft for seven fantasy seasons based on the favorable draft position rankings by win percentage. This process simulates the entire league draft history (12 players for seven seasons) 1,000 times to get a sense of how the favorable draft position rankings in the league compare to the distrbution of favorable draft position rankings in the 1,000 simulation runs.
Simulation Code
Code
#Create the dataframe of the favorable draft order from the predecited win pctfav_draft_order <- predicted_win_pct_draft_order_12 |> dplyr::select(1:2)#Create draft simulation function draft_sim_function <-function(draft_ranking_df, league_size, number_seasons) {#Create league players for the simulation players_df <-seq(from =1, to = league_size, by =1) |>as.data.frame() |> dplyr::rename("player_id"=1) |> dplyr::mutate(player_id =paste("player", player_id, " "))#Perform repeat sampling of the draft sim <- infer::rep_sample_n(players_df, size = league_size,replace =FALSE,reps = number_seasons)#Create draft pick order as seq from 1 to league size, #repeated for the number of desired seasons sim$draft_pick_order <-as.factor(rep(seq(from =1, to = league_size, by =1), times = number_seasons))#Join with favorable draft order sim <-left_join(sim, draft_ranking_df,by ="draft_pick_order")#Compute last place last <-nrow(players_df)#Compute bottom 4 bottom_four <- (last -3)#Compute the favorable draft position statistics #by player id sim_rankings <- sim |> dplyr::group_by(player_id) |> dplyr::summarise(average_fav_pos =mean(favorable_draft_position),median_fav_pos =median(favorable_draft_position),count_worst =sum(favorable_draft_position == last),count_bottom_4 =sum(favorable_draft_position >= bottom_four)) |> dplyr::ungroup()#Return draft order sim rankings return(sim_rankings)}#Do the simulation with 1000 repetitions - fitting the function#Set seed for replication - i.e todays dateset.seed(11242025)sims_output <-replicate(1000, #set arguments for the functiondraft_sim_function(draft_ranking_df = fav_draft_order,league_size =12,number_seasons =7),#simplify = FALSE returns a list simplify =FALSE)#rbind results together, with names_to generating the simulation run numbersims_df <- purrr::list_rbind(sims_output, names_to ="sim_run")
Simulation Results
Code
#Create histogram of average draft position across draft position#Set dataframe for annotation argumentsannotations_avg <-data.frame(x =c(4.57,mean(sims_df$average_fav_pos), 7.57),y =c(1000, 1200, 1000),label =c("Player 17", "Average", "Player 7")) #Histogram of resultsFigure_7 <-ggplot(sims_df, aes(average_fav_pos)) +geom_histogram(color ="black", fill ="lightgrey") +scale_x_continuous(breaks =seq(from=1, to =12, by =.5)) +#Lower confidence interval ggplot2::geom_vline(xintercept =quantile(sims_df$average_fav_pos, probs = .025),color ="red", size = .75, linetype =2) +#Upper confidence interval ggplot2::geom_vline(xintercept =quantile(sims_df$average_fav_pos, probs = .975),color ="red", size = .75, linetype =2) +#Add in Player 17 ggplot2::geom_vline(xintercept =4.57,color ="blue", size = .75, linetype =2) +#Add in Player 17 ggplot2::geom_vline(xintercept =mean(sims_df$average_fav_pos),color ="orange", size = .75, linetype =2) +#Add in Player 7 ggplot2::geom_vline(xintercept =7.57,color ="green", size = .75, linetype =2) +labs(title ="Figure 7: Average Favorable Draft Ranking: 1000 simulated seven season drafts",x ="Average Favorable Draft Ranking",y ="Count",caption ="The dashed red lines represent 95% confidence intervals around the Mean Average Favorable Draft Ranking. ") +geom_label(data = annotations_avg, aes(x = x, y = y, label = label), size =3.5, fontface ="bold") +theme_classic()Figure_7
In Figure 7, I plot a histogram of the distribution of the average favorable draft rankings after simulating 1000 seven season drafts with 12 players. The blue line represents Player 7’s average favorable draft ranking (the most favorable), while the green line represents Player 17’s average favorable draft ranking (the least favorable). The orange line indicates the average of all of the average favorable draft rankings of the 1000 simulations and the red lines represent the 95% confidence interval around the average of the simulation runs. Both draft rankings fall within the 95% confidence interval (the middle 95% of the distribution) of the average favorable draft rankings, indicating that neither Player 17 nor Player 7’s average draft ranking departs signigicantly from the average of 1000 simulations.
Code
#Set the annotations arguments for the medianannotations_median <-data.frame(x =c(2 , mean(sims_df$median_fav_pos), 9),y =c(1800, 1800, 1800),label =c("Player 17", "Average", "Player 7")) #Histogram of resultsFigure_8 <-ggplot(sims_df, aes(median_fav_pos)) +geom_histogram(color ="black", fill ="lightgrey", bins =12) +scale_x_continuous(breaks =seq(from=1, to =12, by =1)) +#Lower confidence interval ggplot2::geom_vline(xintercept =quantile(sims_df$median_fav_pos, probs = .025),color ="red", size = .75, linetype =2) +#Upper confidence interval ggplot2::geom_vline(xintercept =quantile(sims_df$median_fav_pos, probs = .975),color ="red", size = .75, linetype =2) +#Add in Player 17 ggplot2::geom_vline(xintercept =2,color ="blue", size = .75, linetype =2) +#Add in Player 17 ggplot2::geom_vline(xintercept =mean(sims_df$median_fav_pos),color ="orange", size = .75, linetype =2) +#Add in Player 7 ggplot2::geom_vline(xintercept =9,color ="green", size = .75, linetype =2) +labs(title ="Figure 8: Median Favorable Draft Ranking: 1000 simulated seven season drafts",x ="Median Favorable Draft Ranking",y ="Count",caption ="The dashed red lines represent 95% confidence intervals around the average Median Favorable Draft Ranking.") +geom_label(data = annotations_median, aes(x = x, y = y, label = label), size =3.5, fontface ="bold") +theme_classic()Figure_8
In Figure 8, I plot a histogram of the distribution of the median favorable draft ranking after simulating 1000 seven season drafts with 12 players. The blue line again represents Player 17’s median favorable draft ranking, while the green line represents Player 7’s median favorable draft ranking. The orange line again indicates the average of all of the median favorable draft rankings of the 1000 simulations and the red lines represent the 95% confidence interval around the average of the median of the simulation runs. In this case, Player 17’s median favorable draft ranking of 2 falls outside the 95% confidence intervals between median draft ranking 3 (lower bound) and median draft ranking 10 (upper bound).
Note here that the average median favorable draft ranking is 6.5 (orange line), which is a little misleading because it doesn’t really make sense to think of an average of values that consist only of whole numbers. What is important about this metric is that it indicates that the most common simulated median favorable draft positions are 6 and 7, which are values in the middle of the distribution, with the middle 95% of the favorable median draft positions falling between 3 and 10. In this case, Player 17 falls outside of the middle 95% of this distribution of simulated median favorable draft rankings. This indicates that Player 17 has had an especially lucky median draft ranking position relative to the average median draft ranking from 1000 simulated drafts. On the other hand, Player 7’s median draft ranking of 9 is close to, but not outside, the upper confidence interval of 10.
If we consider how extreme Player 17’s median favorable draft position is relative to the entire distribution, then 98% of all the simulated median favorable draft position are less favorable than the actual median favorable draft position for Player 17 (see code and table below).
Code to calculate Player 17’s Median Favorable Draft Ranking vs. the simulated distribution
Code
#create a cumulative distribution of the median favorable draft rankingscdf_median <-ecdf(sims_df$median_fav_pos)#percent of simulations at or lower than median ranking 2 (Player 17)#i.e. those more favorableperc_lower <-round(cdf_median(2), digits =3)#percent of simulations higher (less favorable) than median draft ranking of 2perc_higher <-round(1-cdf_median(2), digits =3 )#Make Tabletable <-tibble("Term"=c("Percent at or Lower (as or more favorable)", "Percent Higher (less favorable)"),"Value"=c(paste0(perc_lower *100, "%"), paste0(perc_higher *100, "%")))table_4 <- gt::gt(table) |> gt::tab_header(title="Table 4: Percent of simulations lower and higher than Player 17's median favorable draft position") |> gt::tab_source_note(source_note ="Player 17's median favorable draft position was 2 out of 12 total draft positions.") |> gt::tab_options(row.striping.include_table_body =FALSE,quarto.disable_processing =TRUE)table_4
Table 4: Percent of simulations lower and higher than Player 17's median favorable draft position
Term
Value
Percent at or Lower (as or more favorable)
1.9%
Percent Higher (less favorable)
98.1%
Player 17's median favorable draft position was 2 out of 12 total draft positions.
Does Draft Position Matter For Fantasy Football: Key Takeaways
1).Draft positions appear to matter for win percentage and wins, but the impact is less clear for points for. The regression results indicate that draft order has a stastically significant impact on win percentage and season wins, albeit only for one draft position, but the impact is not statistically significant for points for. The wide confidence intervals in the ATE estimates for points for indicates significant uncertainty in the estimates, but it may certainly be the case that large differences in points for are enough to impact season wins and win percentage.
2).Draft position 4 has more season wins and higher average win percentages (p < .05), while draft positions 7 and 9, followed by 2, and 8 are consistently bad draft positions. Why draft position 4 is such a great draft position is a mystery and points to the wide variatian in fantasy draft outcomes. For draft positions 7, 8, and 9, these positions might be far enough in the draft order that players who draft at these positions struggle to pick up good players in early rounds, yet receive limited benefit from the snake format that benefits players at the end of the draft order. Why draft position 2 is such a bad draft position is more of a mystery, but it may be the case that players who draft second are overwhelmed by a “paradox of choice”. With the clear best player taken off the board, players at draft position 2 may end up choosing poorly, overwhelmed by a large amount of great, but not fantastic draft choices.
3).While draft order is random every year, certain players may receive “lucky” or “unlucky” draft positions. Because draft order is random every year, but draft positions are repeatable, certain players may receive especially lucky or unlucky draft positions. While this should even out, at least in theory, as more fantasy football drafts are conducted, the simulations reveal that certain players (Player 17) have received an especially favorable median draft ranking relative to what would be expected under 1000 simulated league draft processes.
4). Draft Order is far from destiny and may have a small impact in practice. While draft position does appear to have a limited impact on fantasy football outcomes, players appear quite able to overcome or squander favorable or unfavorable draft positions. That Player 17 has performed near or at the bottom of the league despite especially lucky draft positions is evidence that injuries, player skill, knowledge, and effort can induce wide variation in fantasy outcomes. Likewise, players who have received relatively poor draft positions, Player 7, Player 8, and Player 9, have all perfomed at the average of the league, indicating that poor draft positions don’t cripple fantasy outcomes. This all suggests that fantasy draft order may have only a small impact on season outcomes.
5). A larger sample size, with a shorter time-frame, is needed to confirm the results. Although the repeat randomization of draft order overtime allows for the comparison of individual perfomance year to year with a different draft position, the yearly passage of time makes comparisons of a player’s performance in different years suspect. In addition, the relatively small sample size leaves me a little bit nervous about large differences being detected due to low statitical power rather than because specific draft positions do have a large causal impact on fantasy football outcomes. Conversely, moderate and meaningful effect sizes, such as those exhibted by draft positions 7 and 9, are likely not detectable with such a small sample size. The results also depend on setting a reasonable reference level for comparison between draft order categories, which can make differences look bigger or smaller depending on which reference category they are compared to.
In the future, someone with access to more fantasy football data and league format history could confirm my results with a much larger sample size, potentially for a short time-frame to ensure that the order of treatment history (draft order) doesn’t matter for the results. Finally, a more principled approach would be to think more carefully about why certain fantasy draft positions would be especially good or bad in certain draft formats and to attempt to develop a theory and reasonable hypotheses about why this is the case.